package api

import (
	"context"
	"fmt"
	"github.com/eko/gocache/lib/v4/cache"
	"github.com/robfig/cron/v3"
	"sync"
)

type Cli interface {
	Api

	Start(context.Context) error

	Stop() error
}

type Api interface {
	GetAccessToken(context.Context) (*AccessToken, error)

	GetStableAccessToken(context.Context, bool) (*AccessToken, error)

	Code2Session(context.Context, string, string) (*Code2Session, error)

	GetPhoneNumber(context.Context, string) (*PhoneNumber, error)
}

type BaseMpApi interface {
	GetAccessToken(*MpConfig) (*AccessToken, error)

	GetStableAccessToken(*MpConfig, bool) (*AccessToken, error)

	Code2Session(*MpConfig, *AccessToken, string, string) (*Code2Session, error)

	GetPhoneNumber(*AccessToken, string) (*PhoneNumber, error)
}

var m = make(map[ProviderType]BaseMpApi)

func RegisterBaseMpApi(cliType ProviderType, cli BaseMpApi) {
	if _, ok := m[cliType]; ok {
		panic(fmt.Sprintf("cli type %s already exist", cliType))
	}
	m[cliType] = cli
}

type ApiCli struct {
	ns                string
	baseApi           BaseMpApi
	config            *MpConfig
	cacheManager      *cache.Cache[*AccessToken]
	checkTokenExpired *cron.Cron
	lock              sync.Mutex
}

func NewApiCli(opts ...Option) Cli {
	ac := &ApiCli{
		ns:                "mp",
		checkTokenExpired: cron.New(cron.WithSeconds()),
	}
	for _, v := range opts {
		v(ac)
	}

	if a, ok := m[ac.config.Provider]; ok {
		ac.baseApi = a
	} else {
		panic(fmt.Sprintf("unsupport provider type %s", ac.config.Provider))
	}
	return ac
}

func (ac *ApiCli) Start(ctx context.Context) error {
	if ac.baseApi == nil {
		return fmt.Errorf("base api not exist")
	}

	if ac.cacheManager == nil {
		return fmt.Errorf("cache manager not exist")
	}
	ac.GetAccessToken(ctx)
	if ac.config.Spec != "" {
		_, err := ac.checkTokenExpired.AddFunc(ac.config.Spec, func() {
			_, err := ac.GetAccessToken(ctx)
			if err != nil {
				// todo error print ... or retry
				return
			}
		})
		if err != nil {
			return err
		}
		ac.checkTokenExpired.Start()
	}
	return nil
}

func (ac *ApiCli) Stop() error {
	if ac.checkTokenExpired != nil {
		ac.checkTokenExpired.Stop()
	}
	return nil
}

func (ac *ApiCli) getAccessTokenKey() string {
	return fmt.Sprintf("%s-%s", ac.ns, "access_token")
}

func (ac *ApiCli) GetAccessToken(ctx context.Context) (at *AccessToken, er error) {
	//double check
	key := ac.getAccessTokenKey()
	at, er = ac.cacheManager.Get(ctx, key)
	// if er != nil {
	// 	return nil, er
	// }
	if at != nil && !at.Expired() {
		return at, nil
	}

	ac.lock.Lock()
	defer ac.lock.Unlock()

	if at != nil && !at.Expired() {
		return at, nil
	}

	at, er = ac.baseApi.GetAccessToken(ac.config)
	if er == nil {
		err := ac.cacheManager.Set(context.Background(), key, at)
		if err != nil {
			panic(er)
		}
	}
	return
}

func (ac *ApiCli) GetStableAccessToken(ctx context.Context, force bool) (at *AccessToken, er error) {
	if at, er = ac.baseApi.GetStableAccessToken(ac.config, force); er == nil {
		key := ac.getAccessTokenKey()
		err := ac.cacheManager.Set(ctx, key, at)
		if err != nil {
			return nil, err
		}
	}
	return
}

func (ac *ApiCli) Code2Session(ctx context.Context, js_code, anonymous_code string) (acs *Code2Session, e error) {
	if at, e := ac.GetAccessToken(ctx); e == nil {
		return ac.baseApi.Code2Session(ac.config, at, js_code, anonymous_code)
	}
	return
}

func (ac *ApiCli) GetPhoneNumber(ctx context.Context, code string) (aph *PhoneNumber, e error) {
	if at, e := ac.GetAccessToken(ctx); e == nil {
		return ac.baseApi.GetPhoneNumber(at, code)
	}
	return
}
